package cn.jaye.fabric.domain.service;

import cn.jaye.fabric.common.constants.FarbicConstant;
import cn.jaye.fabric.domain.model.ChainCode;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.ProposalException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.*;
import java.util.concurrent.CompletableFuture;

import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;

/**
 * @author Jaye
 * @create 2018-05-07 17:08
 */
@Service
public class ChainCodeService {

    private Logger logger = LoggerFactory.getLogger(ChainCodeService.class);

    @Autowired
    private FarbicConstant farbicConstant;

    @Autowired
    private HFClient hfClient;

    @Autowired
    private ConfigService configService;


    private ChaincodeID createChainCode(String chainCodeName, String path) {
        return ChaincodeID.newBuilder().setName(chainCodeName)
                .setVersion(farbicConstant.getChainCodeDefaultVersion())
                .setPath(path).build();
    }

    private ChaincodeID createChainCode(String chainCodeName, String chainCodeVersion, String chainCodePath) {
        if (StringUtils.isBlank(chainCodeVersion)) {
            if (StringUtils.isNotBlank(chainCodePath)) {
                return createChainCode(chainCodeName, chainCodePath);
            }
            return createChainCode(chainCodeName);
        }
        if (StringUtils.isBlank(chainCodePath)) {
            return ChaincodeID.newBuilder().setName(chainCodeName)
                    .setVersion(chainCodeVersion)
                    .setPath(farbicConstant.getChainCodeDefaultPath()).build();
        }
        return ChaincodeID.newBuilder().setName(chainCodeName)
                .setVersion(chainCodeVersion)
                .setPath(chainCodePath).build();
    }

    private ChaincodeID createChainCode(String chainCodeName) {
        return ChaincodeID.newBuilder().setName(chainCodeName)
                .setVersion(farbicConstant.getChainCodeDefaultVersion())
                .setPath(farbicConstant.getChainCodeDefaultPath()).build();
    }

    public ProposalResponse install(Channel channel, ChainCode chainCode) throws InvalidArgumentException, ProposalException {
        Collection<ProposalResponse> responses;
        Collection<ProposalResponse> successful = new LinkedList<>();
        Collection<ProposalResponse> failed = new LinkedList<>();
        ChaincodeID chaincodeID = createChainCode(chainCode.getName(), chainCode.getVersion(), chainCode.getPath());
        InstallProposalRequest installProposalRequest = hfClient.newInstallProposalRequest();
        installProposalRequest.setChaincodeID(chaincodeID);
        installProposalRequest.setChaincodeName(chaincodeID.getName());
        String chainCodeLocation = chainCode.getLocation();
        if (StringUtils.isBlank(chainCodeLocation)) {
            chainCodeLocation = farbicConstant.getChainCodeSourceLocation();
        }
        File file = new File(chainCodeLocation);
        installProposalRequest.setChaincodeSourceLocation(file);
        installProposalRequest.setChaincodeVersion(chaincodeID.getVersion());

        logger.info("Sending install proposal");

        int numInstallProposal = 0;

        numInstallProposal = numInstallProposal + channel.getPeers().size();
        responses = hfClient.sendInstallProposal(installProposalRequest, channel.getPeers());

        for (ProposalResponse response : responses) {
            if (response.getStatus() == ProposalResponse.Status.SUCCESS) {
                logger.info("Successful install proposal response Txid: %s from peer %s", response.getTransactionID(), response.getPeer().getName());
                successful.add(response);
            } else {
                failed.add(response);
            }
        }
        SDKUtils.getProposalConsistencySets(responses);
        logger.info("Received %d install proposal responses. Successful+verified: %d . Failed: %d", numInstallProposal, successful.size(), failed.size());
        ProposalResponse first;
        if (failed.size() > 0) {
            first = failed.iterator().next();
            fail("Not enough endorsers for install :" + successful.size() + ".  " + first.getMessage());
        } else {
            first = successful.iterator().next();
        }
        return first;
    }

    public CompletableFuture<BlockEvent.TransactionEvent> instantiate(Channel channel, ChainCode chainCode) throws Exception {
        ChaincodeID chaincodeID = createChainCode(chainCode.getName(), chainCode.getVersion(), chainCode.getPath());
        Collection<ProposalResponse> responses;
        Collection<ProposalResponse> successful = new LinkedList<>();
        Collection<ProposalResponse> failed = new LinkedList<>();

        InstantiateProposalRequest instantiateProposalRequest = hfClient.newInstantiationProposalRequest();
        instantiateProposalRequest.setProposalWaitTime(farbicConstant.getChannelProposalWaitTime());
        instantiateProposalRequest.setChaincodeID(chaincodeID);
        instantiateProposalRequest.setFcn("init");
        instantiateProposalRequest.setArgs(chainCode.getArgs());
        instantiateProposalRequest.setChaincodeName(chaincodeID.getName());
        Map<String, byte[]> tm = new HashMap<>();
        tm.put("HyperLedgerFabric", "InstantiateProposalRequest:JavaSDK".getBytes(UTF_8));
        tm.put("method", "InstantiateProposalRequest".getBytes(UTF_8));
        instantiateProposalRequest.setTransientMap(tm);

        ChaincodeEndorsementPolicy chaincodeEndorsementPolicy = new ChaincodeEndorsementPolicy();
        chaincodeEndorsementPolicy.fromYamlFile(new File(farbicConstant.getChainCodeEndorsementPolicy()));
        instantiateProposalRequest.setChaincodeEndorsementPolicy(chaincodeEndorsementPolicy);

        logger.info("Sending instantiateProposalRequest to all peers with arguments");
        successful.clear();
        failed.clear();

        responses = channel.sendInstantiationProposal(instantiateProposalRequest, channel.getPeers());

        for (ProposalResponse response : responses) {
            if (response.isVerified() && response.getStatus() == ProposalResponse.Status.SUCCESS) {
                successful.add(response);
                logger.info("Succesful instantiate proposal response Txid: %s from peer %s", response.getTransactionID(), response.getPeer().getName());
            } else {
                failed.add(response);
            }
        }
        logger.info("Received %d instantiate proposal responses. Successful+verified: %d . Failed: %d", responses.size(), successful.size(), failed.size());
        if (failed.size() > 0) {
            ProposalResponse first = failed.iterator().next();
            fail("Not enough endorsers for instantiate :" + successful.size() + "endorser failed with " + first.getMessage() + ". Was verified:" + first.isVerified());
        }
        return channel.sendTransaction(successful, channel.getOrderers());
    }

    public String invoke(Channel channel, ChainCode chainCode) throws Exception {
        ChaincodeID chaincodeID = createChainCode(chainCode.getName(), chainCode.getVersion(), chainCode.getPath());
        Collection<ProposalResponse> responses;
        Collection<ProposalResponse> successful = new LinkedList<>();
        Collection<ProposalResponse> failed = new LinkedList<>();
        TransactionProposalRequest transactionProposalRequest = hfClient.newTransactionProposalRequest();
        transactionProposalRequest.setChaincodeID(chaincodeID);
        transactionProposalRequest.setFcn(chainCode.getFcn());
        transactionProposalRequest.setProposalWaitTime((int) configService.getProposalProperties().get("ProposalWaitTime"));
        for (String str : chainCode.getArgs()) {
            System.out.print(str);
        }
        transactionProposalRequest.setArgs(chainCode.getArgs());
        Map<String, byte[]> tm2 = new HashMap<>();
        tm2.put("HyperLedgerFabric", "TransactionProposalRequest:JavaSDK".getBytes(UTF_8));
        tm2.put("method", "TransactionProposalRequest".getBytes(UTF_8));
        tm2.put("result", ":)".getBytes(UTF_8));  /// This should be returned see chaincode.
        transactionProposalRequest.setTransientMap(tm2);

        logger.info("sending transactionProposal to all peers with arguments: " + chainCode.getFcn());

        Collection<ProposalResponse> transactionPropResp = channel.sendTransactionProposal(transactionProposalRequest);
        for (ProposalResponse response : transactionPropResp) {
            if (response.getStatus() == ProposalResponse.Status.SUCCESS) {
                logger.info("Successful transaction proposal response Txid: %s from peer %s", response.getTransactionID(), response.getPeer().getName());
                successful.add(response);
            } else {
                failed.add(response);
            }
        }
        //
        Collection<Set<ProposalResponse>> proposalConsistencySets = SDKUtils.getProposalConsistencySets(transactionPropResp);
        if (proposalConsistencySets.size() != 1) {
            fail(format("Expected only one set of consistent proposal responses but got %d", proposalConsistencySets.size()));
        }

        logger.info("Received %d transaction proposal responses. Successful+verified: %d . Failed: %d",
                transactionPropResp.size(), successful.size(), failed.size());
        if (failed.size() > 0) {
            ProposalResponse firstTransactionProposalResponse = failed.iterator().next();
            fail("Not enough endorsers :" + failed.size() + " endorser error: " +
                    firstTransactionProposalResponse.getMessage() +
                    ". Was verified: " + firstTransactionProposalResponse.isVerified());
        }
        logger.info("Successfully received transaction proposal responses.");
        ProposalResponse resp = transactionPropResp.iterator().next();
        byte[] x = resp.getChaincodeActionResponsePayload(); // This is the data returned by the chaincode.
        String resultAsString = null;
        if (x != null) {
            resultAsString = new String(x, "UTF-8");
        }
//        channel.sendTransaction(successful).get((int) configService.getProposalProperties().get("InvokeWaitTime"), TimeUnit.SECONDS);
        channel.sendTransaction(successful, channel.getOrderers());
        return resultAsString;
    }

    public String query(Channel channel, ChainCode chainCode) throws InvalidArgumentException, ProposalException {
        ChaincodeID chaincodeID = createChainCode(chainCode.getName(), chainCode.getVersion(), chainCode.getPath());
        QueryByChaincodeRequest queryByChaincodeRequest = hfClient.newQueryProposalRequest();
        queryByChaincodeRequest.setArgs(chainCode.getArgs());
        queryByChaincodeRequest.setFcn(chainCode.getFcn());
        queryByChaincodeRequest.setChaincodeID(chaincodeID);
        queryByChaincodeRequest.setChaincodeName(chaincodeID.getName());

        Map<String, byte[]> tm = new HashMap<>();
        tm.put("HyperLedgerFabric", "QueryByChaincodeRequest:JavaSDK".getBytes(UTF_8));
        tm.put("method", "QueryByChaincodeRequest".getBytes(UTF_8));
        queryByChaincodeRequest.setTransientMap(tm);

        Collection<ProposalResponse> queryProposals = channel.queryByChaincode(queryByChaincodeRequest, channel.getPeers());

//        Collection<ProposalResponse> successful = Lists.newArrayList();
        Collection<ProposalResponse> failed = Lists.newArrayList();
        for (ProposalResponse proposalResponse : queryProposals) {
            if (!proposalResponse.isVerified() || proposalResponse.getStatus() != ProposalResponse.Status.SUCCESS) {
                failed.add(proposalResponse);
            } else {
                String payload = proposalResponse.getProposalResponse().getResponse().getPayload().toStringUtf8();
                logger.info("Query payload of b from peer %s returned %s", proposalResponse.getPeer().getName(), payload);
                return payload;
            }
        }
        return null;
    }

}
